package com.puzzlebazar.client.puzzleinfo;

import com.google.gwt.user.client.rpc.AsyncCallback;
import com.puzzlebazar.client.puzzleinfo.PuzzleInfo.Status;
import com.puzzlebazar.client.services.PuzzleServiceAsync;
import com.puzzlebazar.client.services.ServiceFactory;
import com.puzzlebazar.client.util.ConnectionLock;
import com.puzzlebazar.client.util.ObservableImpl;
import com.puzzlebazar.client.util.AsyncCallbackLock;

public class PuzzleInfoModel extends ObservableImpl {

	private final AsyncCallback<PuzzleInfo> puzzleInfoCallback = new PuzzleInfoCallback();
	private final AsyncCallback<PuzzleAndHistoryInfo> tutorialInfoCallback = new TutorialInfoCallback();
	private final AsyncCallback<Boolean> puzzleRenameCallback = new PuzzleRenameCallback();
	private final AsyncCallback<Boolean> changeStatusCallback = new ChangeStatusCallback();

	private String puzzleKey = null;
	private PuzzleInfo puzzleInfo = null;
	private HistoryInfo historyInfo = null;
	private boolean tutorial = false;
	
	private boolean refreshing = false;
	
	/**
	 * @return The current puzzle Key associated with this model
	 */
	public String getPuzzleKey() {
		return puzzleKey;
	}


	/**
	 * @return The key of the associated puzzle info
	 */
	public String getPuzzleInfoKey() {
		if( puzzleInfo == null )
			return null;
		return puzzleInfo.getKey();
	}

	/**
	 * @return The key of the last completed history
	 */
	public String getSolvedHistoryKey() {
		if( puzzleInfo == null )
			return null;
		return puzzleInfo.getSolvedByUserHistoryKey();
	}

	/**
	 * @return The key of the last started history
	 */
	public String getStartedHistoryKey() {
		if( puzzleInfo == null )
			return null;
		return puzzleInfo.getStartedByUserHistoryKey();
	}

	/**
	 * @return The key of the history attached with this tutorial, null if its not a tutorial
	 */
	public String getTutorialHistoryKey() {
		if( historyInfo == null )
			return null;
		return historyInfo.getKey();
	}
	
	/**
	 * Associates a puzzle key with this model. 
	 * Call notifyObservers().
	 * 
	 * @param puzzleKey The puzzle key associated with this model (null if no puzzle associated)
	 */
	public void setPuzzleKey(String puzzleKey) {
		if( (puzzleKey == null  && this.puzzleKey != null) ||
			!puzzleKey.equals( this.puzzleKey ) || 
			tutorial ) {
			tutorial = false;
			this.puzzleKey = puzzleKey;
			puzzleInfo = null;
			historyInfo = null;
			setChanged();
		
			if( puzzleKey != null ) {		
				PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
				refreshing = true;
				puzzleService.loadPuzzle( puzzleKey, puzzleInfoCallback  );
			}
		}
	}	

	/**
	 * Associates a puzzle key for a tutorial with this model. 
	 * Call notifyObservers().
	 * 
	 * @param puzzleKey The puzzle key associated with this model (null if no puzzle associated)
	 */
	public void setTutorialPuzzleKey(String puzzleKey) {
		if( (puzzleKey == null  && this.puzzleKey != null ) ||
			!puzzleKey.equals( this.puzzleKey ) || 
			!tutorial ) {
			tutorial = true;
			this.puzzleKey = puzzleKey;
			puzzleInfo = null;
			setChanged();
		
			if( puzzleKey != null ) {		
				PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
				refreshing = true;
				puzzleService.loadTutorial( puzzleKey, tutorialInfoCallback  );
			}
		}
	}
	
	/**
	 * Forces a reload of the puzzle information
	 * Call notifyObservers().
	 */
	public void reload() {
		if( puzzleKey == null || puzzleInfo == null || isRefreshing() ) 
			return;
		
		puzzleInfo = null;
		setChanged();
	
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		refreshing = true;
		if( tutorial )
			puzzleService.loadTutorial( puzzleKey, tutorialInfoCallback );
		else
			puzzleService.loadPuzzle( puzzleKey, puzzleInfoCallback );
	}	

	/**
	 * Rename the puzzle associated with this model. The rename will fail directly if
	 * there is no associated puzzle information or if the user doesn't have the right
	 * to edit the puzzle. The rename operation can also fail server-size in which
	 * case this method will return true but the name will not be permanently changed.
	 * Call notify observers after this call.
	 * 
	 * @param title The new title for this puzzle
	 * @return false If rename operation could not be performed due to lack of privilege 
	 *   (Note: can fail even if this method return true, if failure occurs server-side.)
	 */
	public boolean renamePuzzle(String title) {
		if( puzzleInfo == null )
			return false;
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleInfo.setTitle(title);
		setChanged();
		puzzleService.renamePuzzleOrTutorial( puzzleKey, title, puzzleRenameCallback  );		
		return true;
	}


	/**
	 * Delete the associated puzzle
	 *
	 * @param deletingLock The associated lock
	 */
	public void deletePuzzle(ConnectionLock deletingLock) {
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.deletePuzzleOrTutorial(puzzleInfo.getKey(), new AsyncCallbackLock<Boolean>(deletingLock) );
	}

	/**
	 * Publish the puzzle before a review
	 * Call notify observers after this call.
	 */
	public void publishPuzzleBeforeReview() {
		puzzleInfo.setStatus( Status.PUBLISHED );
		setChanged();		
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.changePuzzleOrTutorialStatus(puzzleInfo.getKey(),  Status.PUBLISHED, changeStatusCallback );
	}

	/**
	 * Publish the puzzle after a review
	 * Call notify observers after this call.
	 */
	public void publishPuzzleAfterReview() {
		puzzleInfo.setStatus( Status.REVIEW_PUBLISHED );
		setChanged();		
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.changePuzzleOrTutorialStatus(puzzleInfo.getKey(),  Status.REVIEW_PUBLISHED, changeStatusCallback );
	}

	/**
	 * Send the puzzle to be reviewed
	 * Call notify observers after this call.
	 */
	public void sendPuzzleForReview() {
		puzzleInfo.setStatus( Status.IN_REVIEW );
		setChanged();
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.changePuzzleOrTutorialStatus(puzzleInfo.getKey(), Status.IN_REVIEW, changeStatusCallback );
	}

	/**
	 * Send the puzzle to be reviewed
	 * Call notify observers after this call.
	 */
	public void acceptPuzzleAfterReview() {
		puzzleInfo.setStatus( Status.ACCEPTED );
		setChanged();
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.changePuzzleOrTutorialStatus(puzzleInfo.getKey(), Status.ACCEPTED, changeStatusCallback );		
	}

	/**
	 * Send the puzzle to be reviewed
	 * Call notify observers after this call.
	 */
	public void makePuzzlePrivate() {
		puzzleInfo.setStatus( Status.PRIVATE );
		setChanged();
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.changePuzzleOrTutorialStatus(puzzleInfo.getKey(), Status.PRIVATE, changeStatusCallback );		
	}
	
	/**
	 * Access the current puzzle information for the specified puzzle key. Will be
	 * null if the puzzle information is not yet loaded or if the puzzle key is invalid.
	 * Use isRefreshing() to distinguish between those cases.
	 * 
	 * @return The information for the specified puzzle key
	 */
	public PuzzleInfo getPuzzleInfo() {
		return puzzleInfo;
	}

	/**
	 * Access the current history information for the specified puzzle key for a tutorial. 
	 * Will be null if the history information is not yet loaded, if the puzzle key is invalid
	 * or if a puzzle is loaded instead of a tutorial.
	 * Use isRefreshing() to distinguish between those cases.
	 * 
	 * @return The information for the specified puzzle key
	 */
	public HistoryInfo getTutorialHistoryInfo() {
		return historyInfo;
	}

	/**
	 * @return True if the puzzle list is waiting to be refreshed by an asynchronous call
	 */
	public boolean isRefreshing() {
		return refreshing;
	}

	private class PuzzleInfoCallback implements AsyncCallback<PuzzleInfo> {

		@Override
		public void onFailure(Throwable caught) {
			failed();
		}

		@Override
		public void onSuccess(PuzzleInfo result) {
			if( result == null )
				failed();
			else {
				refreshing = false;
				puzzleInfo = result;
				historyInfo = null;
				setChanged();
				notifyObservers();
			}
			
		}

		private void failed() {
			refreshing = false;
			puzzleInfo = null;
			historyInfo = null;
			setChanged();
			notifyObservers();
		}
	}

	private class TutorialInfoCallback implements AsyncCallback<PuzzleAndHistoryInfo> {

		@Override
		public void onFailure(Throwable caught) {
			failed();
		}

		@Override
		public void onSuccess(PuzzleAndHistoryInfo result) {
			if( result == null )
				failed();
			else {
				refreshing = false;
				puzzleInfo = result.getPuzzleInfo();
				historyInfo = result.getHistoryInfo();
				setChanged();
				notifyObservers();
			}
			
		}

		private void failed() {
			refreshing = false;
			puzzleInfo = null;
			historyInfo = null;
			setChanged();
			notifyObservers();
		}
	}


	private class PuzzleRenameCallback implements AsyncCallback<Boolean> {

		@Override
		public void onFailure(Throwable caught) {
			if( puzzleInfo != null ) {
				puzzleInfo.setTitle( "Untitled" );
				setChanged();
				notifyObservers();
			}
		}

		@Override
		public void onSuccess(Boolean result) {
			if( puzzleInfo != null && !result )
				puzzleInfo.setTitle( "Untitled" );
			setChanged();
			notifyObservers();
		}

	}


	private class ChangeStatusCallback implements AsyncCallback<Boolean> {

		@Override
		public void onFailure(Throwable caught) {
			if( puzzleInfo != null ) {
				puzzleInfo.setStatus( Status.UNKNOWN );
				setChanged();
				notifyObservers();
			}
		}

		@Override
		public void onSuccess(Boolean result) {
			if( puzzleInfo != null && !result )
				puzzleInfo.setStatus( Status.UNKNOWN );
			setChanged();
			notifyObservers();
		}

	}

	/**
	 * @return True if this model represents a tutorial
	 */
	public boolean isTutorial() {
		return tutorial;
	}





}
